Skip to content

fix(metadata)!: unregisterPackage keeps the rows that name the package by the _packageId stamp alone; detector for 17.0.0–18.3.0 stamped environment rows (#32985) - #33416

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/area-E2-32985
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/area-E2-32985

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Refs #32985

The card stays open (seat ruling 2026-11-14T19:48Z): the follow-up PR keeps column-bound rows an 18.4.0 boot bound unless history shows them stamped since create (kept on undetermined or with no history table), and notes that a future uninstall UI must use unregisterPackageWithReport and surface kept (IMetadataService.unregisterPackage has no force or result).

Clause-②: yes (narrowing) — narrowed: unregisterPackage(P) keeps, until { force: true }, items it used to remove (every item stored in a row with an empty package_id whose body names P by _packageId alone, including every DatabaseLoader-stored object/view/app/page of P); the boot bind and migrateDatabaseLoaderRowPackages leave such rows unbound. Widened: additive in-process surface (unregisterPackageWithReport, { force }, MetadataLoader.stampOnlyRows, detectStampOnlyRows, the 'stamp-only' left reason).
Metadata-protocol: none

BREAKING (review#3 M1, 6883260484): a caller of MetadataManager.unregisterPackage over a DatabaseLoader now keeps and reports items whose stored rows name the package by the _packageId stamp alone, every object/view/app/page item of the package among them. Run detectStampOnlyRows, then pass { force: true } (or use unregisterPackageWithReport) to remove the package's own items as before. The changeset carries the adr-0087 not-required (no-migration-prescription) marker. The any-source guard (wider than the ruling's source='database') was accepted by review because it only errs toward keeping.

维护者速读

  • 改了什么:unregisterPackage 遇到「package_id 列为空、正文只靠 _packageId 印记指向该包」的存储行时,默认保留该条目并报告({ force: true } 才删);启动时的列绑定与运维迁移不再凭这种印记绑定 package_id;新增只读检测器 detectStampOnlyRows,按每行自己的历史判定印记来源;changeset 附升级说明。
  • 为什么改:17.0.0–18.3.0 在「声明 packageId 的 MetadataPlugin + eager 无 artifactSource + DatabaseLoader」组合下,把宿主印记写进了环境自有的行;main 上卸载宿主包会把这些环境数据连行删掉(18.3.0 实建库 37 行删 34 行),属静默数据丢失红线。
  • 风险与回滚:代价是误保留——非按包存储的类型(object/view/app…)的包自有行同样只有印记,卸载时会被保留并 warn,需 force;无数据迁移、无新写入,回滚即 revert 本 PR。
  • 先看:packages/metadata/src/metadata-manager.ts 的 unregisterPackageWithReport(先全量检查、后删除);packages/metadata/src/loaders/database-loader.ts 的 bindReadRowPackage 新跳过与 stampOnlyRows;packages/metadata/src/migrations/detect-stamp-only-rows.ts 的历史链判定。
  • 两分钟验证:pnpm --filter @objectstack/metadata exec vitest run src/loaders/database-loader.test.ts -t 32985,预期所有 metadata: measure the 18.3.0 environment rows that carry the host's stamp in their stored body, and decide whether a stamp-removal migration is owed (follow-up of #32791) #32985 用例通过(18.3.0 形态的行全部保留、force 后删除)。
  • 需要你做的:无,席位落地。

What changes

The binding ruling is the maintainer decision of 2026-11-14 on #32985 (guard + detector + upgrade note; no ownership-changing migration now), as the seat recorded it on the card. Every @objectstack/metadata from 17.0.0 through 18.3.0, on a host whose MetadataPlugin declares its packageId, primed eagerly with no artifactSource, with a DatabaseLoader, saved each package-less row its boot pass read back stamped with the host (_packageId, _provenance). From the row alone such an environment row cannot be told from a row of the host's own item; the #32986 mark does not reach it, because its stored body already names the host.

1. The guard (red line). unregisterPackage(P) (and the new unregisterPackageWithReport(P)) keeps an item, registry entry and rows, when any loader holds a row of it whose package_id column is empty and whose stored body names P by _packageId alone, neither packageId nor the legacy package naming any package (stampOnlyPackageOf, new src/stamp-only-row.ts; DatabaseLoader.stampOnlyRows, new optional MetadataLoader.stampOnlyRows). Every row is checked before the first removal, so a failed read throws and removes nothing. Each kept item is reported in the result (kept, reason: 'stamp-only-row', each row's loader, id and source) and in one warn naming the detector and { force: true }; force removes them as before. The check reads storage at removal time rather than marking registry objects, so no copy a later writer registers can drop it (the hole #33003 had to close for the #32986 mark).

  • Deliberately wider than the ruling's source = 'database': the check takes every writer's row. Measured on the 18.3.0-built fixture, the boot also stamped another writer's package-less row (agent/api_agent, source = 'api'), and main deleted it on unregisterPackage(host). Under 64ce235's reading a body stamp in such a row binds nothing (it is the environment's for every column reader), so keeping it only ever keeps an environment row.
  • unregisterPackageWithReport is a method of its own because the spec's IMetadataService.unregisterPackage answers void and a class method cannot answer more under that signature; the published contract is not widened here.

2. bindReadRowPackage (the boot's column-only bind): it can skip stamp-only rows, and does. FROM binding the empty package_id column of a legacy row whose body names its package by _packageId alone TO leaving it. Pinned both ways: such a row (the host's stamp, or another package's) is not bound and nothing is written; a legacy row naming its package by its own packageId is still bound; and the cost to a genuine pre-#32246 legacy row is pinned by a CONTROL — a row the host's file item seeded is bound on the same boot by that item's own save (the save path's column-only binding, unchanged). What is left unbound is a legacy row no shipped code rewrites; reads of it are unchanged (the loader reads its package off the body).

3. migrateDatabaseLoaderRowPackages leaves the same rows, new reason 'stamp-only'.

4. Read-only detector: detectStampOnlyRows(store, { packageId? }) in @objectstack/metadata/migrations, next to the migration it pairs with and in the shape of the read-only scrubHistoryDrainedKeys count — chosen over an os migrate meta --stored-style CLI because it is the smallest thing an operator can run against a driver today, touches no package outside @objectstack/metadata, and a CLI can wrap it later. It takes find only and reports every row whose body names a package by _packageId alone with its column empty or naming that package (an 18.4.0 boot bound by-package rows to the host), each with a verdict from its own history, walked back from the row's checksum through previous_checksum (so another holder's history of the name is never read as its own): stamp-added (a version naming no package, then one adding the stamp; onlyStampAdded marks the boot's signature), stamped-since-create, or undetermined.

5. Upgrade note in the changeset: affected releases 17.0.0–18.3.0, the composition, the symptoms (rows read as host-owned, phantom 409 holders, deletion on uninstall before this change, the 18.4.0 column bind), the detector, and the manual recipe for a stamp-added row. It makes no claim about first-party reach (the cloud product's MetadataPlugin configuration is owed a separate check).

6. Fold-in (review nit N1 of merged PR #33299): keyedEntryBodies's doc comment now says the order handed on is the keyed entries first, then the unkeyed bodies, FROM loadMany's row order. Text only.

Door FROM TO
unregisterPackage(P) over an item a loader stores in a row with an empty package_id whose body names P by _packageId alone, any source item and rows removed kept and reported (result kept, one warn); nothing removed if a check fails
unregisterPackage(P, { force: true }) — removes them too, as before
unregisterPackage(P) over an item whose body names P by its own packageId / package, or whose column is bound removed removed (unchanged)
boot column-only bind of a legacy row named by _packageId alone bound to that package left empty; a row whose package's code still ships is bound by that item's own save
migrateDatabaseLoaderRowPackages over such a row bound left 'stamp-only'

Tests

Acceptance notes

  • Rows an 18.4.0 boot already bound are outside the guard. 18.4.0 (published 2026-11-13) bound the package_id column of each stamped by-package row to the host at first boot. Such a row now names the host by its column, so the ruling's criterion (no package column) does not hold and unregisterPackage(host) still removes it. The detector reports it (column set, verdict from history). Whether the deletion door should also consult history for column-bound rows is a decision, raised in the report, not taken here.
  • The guard is deletion-only. On the replayed fixture the symptoms the upgrade note names still read the same: a stamped row is listed as the host's, and a bare read of action/helper is still refused 409 ambiguous-in-environment naming the host as a holder, until the row is repaired by hand (recipe in the changeset).
  • False positives are wide for one class: DatabaseLoader writes no column for a type not stored per (package, name), so every row it stores for a package's item of such a type names the package by the stamp alone and is now kept until force. unregisterPackage has no first-party production caller (its CHANGELOG records 0).
  • The detector does not report a row whose column names another package while its body stamp names the host (flow/pkg_flow in the fixture): the column owns it.
  • MetadataManager.register of a by-package item read from a stamp-only row still binds that row's column through the save path's column-only binding when it writes the row (unchanged; the boot no longer does).

Verification

Final head 7617d86917 unless a line says otherwise. Base fca5f89cce.

metadata: typecheck + full suite (head 0ca9978; later commits touch tests only, re-run below)
pnpm --filter @objectstack/metadata typecheck && pnpm --filter @objectstack/metadata test
 Test Files  79 passed (79)
      Tests  2418 passed (2418)
VERDICT command-exit 0

tsc --noEmit --listFiles includes all four edited test files. Detector file re-run at the final head: Tests 8 passed (8).

dependents that use the moved doors (head c90052e)

pnpm --filter @objectstack/objectql --filter @objectstack/runtime --filter @objectstack/metadata-protocol --filter @objectstack/mcp --filter @objectstack/service-datasource test, after building their closures:

objectql             Test Files  595 passed (595)   Tests  12953 passed (12953)
runtime              Test Files  599 passed (599)   Tests  10678 passed | 19 skipped (10697)
metadata-protocol    Test Files  339 passed | 9 skipped (348)   Tests  22848 passed | 43 skipped (22891)
mcp                  Test Files  64 passed (64)     Tests  1174 passed (1174)
service-datasource   Test Files  75 passed (75)     Tests  1317 passed (1317)
VERDICT command-exit 0

Chosen by git grep for every package outside metadata that calls unregisterPackage, bindReadRowPackage, migrateDatabaseLoaderRowPackages or DatabaseLoaderRowLeftReason, or whose tests build a DatabaseLoader. The rest of the 87-package affected set (select-shard-packages.sh, pull_request mode) is Test Core's, in CI.

replay over a real 18.3.0-built SQLite database (the card's measurement fixture, 37 rows)

Same probe script as the measurement, pointed at this branch's build:

                 boot bound to host   unregisterPackage(host) deleted
main  stamped    12                   34
this  stamped    0                    0
main  control    0                    0
this  control    0                    0

Reads identical to main on every probed name; the one write-door difference is the column the boot no longer binds (action/helper's stamped row stays column-empty). Detector over the same file, packageId: com.host.app: 37 scanned, 34 reported — 32 stamp-added (all onlyStampAdded, history 1 then 2), 2 undetermined (rows the fixture inserted raw, without a create in history); the database file's hash is unchanged. Over the never-stamped control: 37 scanned, 0 reported.

ablations (head c90052e, restored; git diff HEAD empty afterwards)

Through scripts/ablation-replace.mjs (anchor hit x1 then x0, replacement landed), each followed by the three pinning files:

A1 unregisterPackage check replaced by an empty row list   Tests  6 failed | 1114 passed
A2 bindReadRowPackage stamp-only skip removed              Tests  5 failed | 1115 passed
A3 migration stamp-only branch disabled                    Tests 12 failed | 1108 passed
gates

The 66 families node scripts/pm/dispatch-gates.mjs --commands derives for this diff were each run at head 7617d86917, with each exit code captured before any pipe: 66 of 66 exited 0. They include check:where-matcher, check:engine-double-contract, check:nul-bytes and check:dual-build-cjs-loads. On a first run, before the dependents' dists were built, check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET); on the run cited here it exited 0.

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-exit.txt
Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3).

eslint --no-inline-config over the 13 changed TypeScript files: exit 0. CI runs the rest: repo-wide pnpm lint, the artifact-roster and wide-population families, and the full Test Core matrix.


Generated by Claude Code

… by the _packageId stamp alone (#32985)

The boot of @objectstack/metadata 17.0.0-18.3.0 on a host declaring its
package saved the host's stamp into the environment's own DatabaseLoader
rows. unregisterPackage now keeps and reports an item whose stored row has
an empty package_id column and names the package by _packageId alone,
unless forced; bindReadRowPackage binds no column from such a row; the
operator migration leaves it stamp-only; detectStampOnlyRows reports each
such row with its history evidence.

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…, the migration's stamp-only reason and the detector (#32985)

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…rough unregisterPackageWithReport; triage the pins the guard moves (#32985)

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
…e host by its stamp alone (#32985)

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata, touching 45 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

35 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json bd439f17fa575f38d810c7d58e6873918924ed9b.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/metadata/src/index.ts) — pages documenting those are invisible to this run
  • 14 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd439f17fa575f38d810c7d58e6873918924ed9b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 16445492d6ae0c92a7525a7ecf6872bf20aac02e — the merge of head 46ee0cd402b1ca1cbbc6fbaad95cafcad2eda2fa into base bd439f17fa575f38d810c7d58e6873918924ed9b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 16445492d6ae0c92a7525a7ecf6872bf20aac02e && git checkout 16445492d6ae0c92a7525a7ecf6872bf20aac02e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bd439f17fa575f38d810c7d58e6873918924ed9b 46ee0cd402b1ca1cbbc6fbaad95cafcad2eda2fa && git checkout -B drift-repro bd439f17fa575f38d810c7d58e6873918924ed9b && git merge --no-ff 46ee0cd402b1ca1cbbc6fbaad95cafcad2eda2fa

node scripts/docs-audit/affected-docs.mjs --json bd439f17fa575f38d810c7d58e6873918924ed9b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bd439f17fa575f38d810c7d58e6873918924ed9b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
Request: review · area E2 · head 7617d86

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 7617d86 · 2026-11-14T19:46:59Z

review#3.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7617d8691743325cdd2fbe7431964b58a0827db0
Local-runs: none
Scope: packages/metadata/src/metadata-manager.ts, packages/metadata/src/stamp-only-row.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/loaders/loader-interface.ts, packages/metadata/src/migrations/bind-database-loader-row-packages.ts, packages/metadata/src/migrations/detect-stamp-only-rows.ts, packages/metadata/src/index.ts, packages/metadata/src/migrations/index.ts, .changeset/32985-stamp-only-rows-guard.md + packages/spec/src/contracts/metadata-service.ts (IMetadataService.unregisterPackage), packages/objectql/src/metadata-facade.ts

① Derived judgments

  • Shape test. stampOnlyPackageOf is true only for a body (flat, or either level of the publish envelope) that carries one _packageId and no packageId or legacy package claim. Combined with the empty-column half at each caller, this is exactly the 17.0.0–18.3.0 boot's signature and the host's own item's shape. They can't be told apart from the row, as the module states.
  • unregisterPackageWithReport. It checks every item's rows across every loader (stampOnlyRows, a thrown read propagates) before the first unregisterAt, so a failed check removes nothing. Kept items stay in the registry and in every loader, are reported per row (loader, id, source), and are logged in one warn that names the remedy. force restores the old path. unregisterPackage keeps its void answer, so IMetadataService's signature is untouched (the options argument is class-level, as MetadataUnregisterOptions is).
  • Binds. bindReadRowPackage returns before binding a stamp-only row, and migrateDatabaseLoaderRowPackages leaves it as 'stamp-only'. The CONTROL shows a real pre-metadata: the artifact door's memory loader and the database loader keep one row per name, so a slotted type's removed or second holder's item is still served (ADR-0150 D4) #32246 legacy row (body packageId) still binds. The replayed 18.3.0 database measures this: main bound 12 and deleted 34 of 37, this head binds 0 and deletes 0.
  • detectStampOnlyRows is read-only (find only) and gives a history verdict per row (stamp-added / stamped-since-create / undetermined).
  • must-fix M1: the narrowing arm is declared as widening. At this head, existing callers see changed outcomes on published surfaces:
    1. MetadataManager.unregisterPackage(P) (exported, and the implementation of IMetadataService.unregisterPackage) keeps items it used to remove: every stamp-only row's item. By the changeset's own account this is also every DatabaseLoader-stored item of P of a type not stored per (package, name) (object, view, app, page …), since those rows never get a column. A host that uninstalls a package through this door now keeps those items registered and stored until it passes { force: true }.
    2. The boot bind and migrateDatabaseLoaderRowPackages leave rows unbound that they used to bind, so readers of package_id see empty where they saw P.
      No input is refused, but an existing call's effect is reduced. That is the same class I ruled a narrowing on fix(plugin-auth)!: the Register SSO Provider bridge takes every endpoint /sso/register takes, and the domain-verification answer carries the registered domain #33314 and fix(setup)!: serve the Audit Log entry to a view_all_audit_log holder who lacks manage_platform_settings #33349 (a changed outcome on a published door for an existing caller), and the direction is not purely additive. Declare Clause-②: yes (narrowing) (the single arm; AGENTS.md), add fix(metadata)! and a BREAKING paragraph whose fix is "run detectStampOnlyRows, then pass { force: true } once the kept rows are the package's own; a package's object/view/app/page items are kept until then", and add one ADR-0087 marker (not-required (no-migration-prescription) fits: no row is rewritten, and the repair is by hand and deferred). The changeset's What-moves table and false-positive paragraph already carry the substance. This is a text fix; the code can stay.
  • nit N1: detectStampOnlyRows reads the whole table (find(table, {})) and then one history find per candidate row. That's fine for an operator tool run once. If an install has many rows, batching the history read by (type, name) would help. Optional.

② Semver level

@objectstack/metadata: patch is right for a non-protocol change even with the narrowing arm (AGENTS.md: "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition"). Metadata-protocol: none holds, since nothing an author writes is accepted or refused differently.

③ Boundary flags

  • No in-tree runtime caller of MetadataManager.unregisterPackage (objectql's facade has its own). The kept-items effect lands on external hosts (cloud uninstall paths) first. That is why the BREAKING banner matters: it is what such a host reads in the CHANGELOG.
  • Past the ruling. The guard covers rows of every source, not only 'database'. The reason is that the 17.0.0–18.3.0 boot also stamped other writers' package-less rows. It errs toward keeping, never toward deleting or binding, so I accept it. The seat should note it beside the maintainer's ruling, together with the two scope questions already raised (18.4.0 column-bound rows; narrowing false positives by history).
  • Folds fix(metadata)!: MetadataManager.loadMany takes a by-package name's bodies and packages off the loader's keyed entries; loadManyKeyed entries carry their package #33299's N1 (the order note in keyedEntryBodies): doc only.
  • CI at this head when read (19:48Z): 12 success, 16 skipped, 20 in progress, none red. The ACCEPT re-reads every check at the landing head.

④ Design judgment

The maintainer's guard ruling picks fail-closed: an environment row is never removed or bound to the host by its stamp, at the cost of keeping some of the host's own rows. This implementation follows that to the letter at all three doors. It answers what it kept rather than staying silent, keeps the escape hatch explicit (force), and keeps the evidence tool read-only until an install reports a non-zero count. The design holds; only the declaration needs to say that the removal and bind doors now do less.

Implemented-by: claude/area-E2-32985
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: FAIL

M1 is text only (changeset arm, !, BREAKING paragraph, ADR-0087 marker, PR body). Same reviewer takes the delta.

…BREAKING note

Review M1 on the stamp-only rows guard: unregisterPackage keeps items it used
to remove until forced, and the bind and the migration leave rows unbound, so
the changeset declares Clause-② yes (narrowing), the `!` subject, a BREAKING
paragraph with the remedy, and the adr-0087 not-required marker.

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(metadata): unregisterPackage keeps the rows that name the package by the _packageId stamp alone; detector for 17.0.0–18.3.0 stamped environment rows (#32985) fix(metadata)!: unregisterPackage keeps the rows that name the package by the _packageId stamp alone; detector for 17.0.0–18.3.0 stamped environment rows (#32985) Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 46ee0cd · 2026-11-14T19:51:29Z

review#3 (delta).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 46ee0cd402b1ca1cbbc6fbaad95cafcad2eda2fa
Local-runs: none
Scope: .changeset/32985-stamp-only-rows-guard.md, packages/metadata/src/metadata-manager.ts, packages/metadata/src/stamp-only-row.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata/src/migrations/bind-database-loader-row-packages.ts + AGENTS.md (changeset declarations)

Delta over my FAIL at 7617d8691743325cdd2fbe7431964b58a0827db0 (comment 6883260484). The diff 7617d869...46ee0cd4 is the changeset only (+6/-2). The code is unchanged.

① Derived judgments

  • M1 fixed. fix(metadata)! and Clause-②: yes (narrowing) (the single arm) now name both narrowed outcomes: unregisterPackage(P) keeps stamp-only items until { force: true }, explicitly including every DatabaseLoader-stored object, view, app and page of P, and the boot bind and the operator migration leave such rows unbound. The additive surface is stated as the widening. The BREAKING paragraph addresses a caller of unregisterPackage over a DatabaseLoader and gives the fix (detectStampOnlyRows, then { force: true } / unregisterPackageWithReport). There is exactly one adr-0087: not-required (no-migration-prescription) marker, which fits because no row is rewritten. The PR title carries !, and the body matches and records the any-source acceptance. The author reports the ADR-0087 and no-major gates pass locally.
  • Everything in my first record's ①/④ holds unchanged at this head.

② Semver level

@objectstack/metadata: patch with BREAKING banner, right for a non-protocol narrowing (AGENTS.md).

③ Boundary flags

  • The body now says Refs #32985 per the seat's ruling. The 18.4.0 column-bound rows go to an immediate follow-up PR.
  • The full dogfood battery owed by the narrowing arm was not re-run locally (author), so the merge queue's dogfood gate carries it, and the ACCEPT must see it green.
  • N1 (detector N+1 history reads) left as optional.
  • The ACCEPT re-reads every check at the landing head.

④ Design judgment

Unchanged: fail-closed at all three doors, as the maintainer's ruling asks, with an explicit force and a read-only evidence tool. It is now declared as what it is.

Implemented-by: claude/area-E2-32985
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
ACCEPT · area E2 (area-lease pilot #33059) · Refs #32985 · head 46ee0cd

  • Review: review#3 PASS at 46ee0cd (record 6883296842), after FAIL 6883260484 on 7617d86 (M1: declare the narrowing; changeset and title only, code unchanged).
  • Governed: check-governed-merges --pr 33416 NOT governed; under 3000 lines.
  • Checks: required checks running at ACCEPT. The red aggregate lanes on 7617d86 were cancelled runs superseded by the push. Auto-merge and the merge queue, with its dogfood regression gate (owed by the narrowing arm), land it only once they pass.
  • The card stays open (seat ruling 19:48Z): the follow-up PR keeps rows an 18.4.0 boot column-bound unless history shows them stamped since create.
  • Landing: maintainer's instruction in the area thread (15:21Z): PRs in this area that passed review go ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 19:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 3825f57 Nov 14, 2026
55 of 56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-E2-32985 branch November 14, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants